package org.beynet.docs.http;

import java.io.FileInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.Callable;
import java.util.regex.Matcher;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.EntityTemplate;
import org.apache.http.entity.StringEntity;
import org.apache.log4j.Logger;
import org.beynet.docs.exceptions.GDocsServerError;
import org.beynet.docs.exceptions.UnexpectedStatusCode;
import org.beynet.docs.session.Session;
import org.beynet.docs.sync.FileToSync;
import org.beynet.docs.sync.FileUploaderHelper;
import org.beynet.docs.sync.SyncStatus;

public class HttpResumableUploader implements Callable<Object> {

    public HttpResumableUploader(FileToSync fileToSync, URI targetURI, Session session, Integer chunksize, boolean resume) {
        this.fileToSync = fileToSync;
        this.targetURI = targetURI;
        this.session = session;
        this.chunksize = chunksize;
        this.resume = resume;
    }

    /**
     * initiate upload process: an URI will be returned by google servers in
     * case of success. The file to upload will be put to this URI.
     *
     * @throws IOException
     * @throws GDocsServerError
     * @throws UnexpectedStatusCode
     */
    public void initiateUpload() throws IOException, GDocsServerError, UnexpectedStatusCode {
        logger.info("initiate upload");
        StringBuilder folder = new StringBuilder("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
        folder.append("<entry xmlns=\"http://www.w3.org/2005/Atom\"  xmlns:docs=\"http://schemas.google.com/docs/2007\">\n");
        folder.append("<title>$TITLE$</title>\n");
        folder.append("</entry>");
        String content = folder.toString();
        String name = fileToSync.getFile().getName();
        name = name.replaceAll("&", "&amp;");
        name = name.replaceAll("<", "&lt;");
        name = name.replaceAll(">", "&gt;");
        content = content.replace("$TITLE$", name);

        HttpPost firstPost = HttpHelper.createPostRequest(session, this.targetURI.toString() + "?convert=false");
        this.mimeType = FileUploaderHelper.getMimeType(fileToSync.getFile());
        firstPost.setHeader("Content-Type", "application/atom+xml");
        firstPost.setHeader("X-Upload-Content-Type", mimeType);
        firstPost.setHeader("X-Upload-Content-Length", new Long(fileToSync.getFile().length()).toString());
        firstPost.setEntity(new StringEntity(content, "UTF-8"));
        HttpClient client = HttpHelper.getHttpClient();
        HttpResponse resp = client.execute(firstPost);
        int status = HttpHelper.getStatusCodeAndReleaseResource(resp);
        if (status != 200) {
            throw new UnexpectedStatusCode(status);
        }
        Header[] location = resp.getHeaders(HttpHelper.LOCATION_HEADER);
        if (location == null || location.length == 0) {
            throw new GDocsServerError("no location header received");
        }
        try {
            uploadURI = new URI(location[0].getValue());
        } catch (URISyntaxException e) {
            throw new GDocsServerError("invalid URI received :" + location[0].getValue());
        }
        logger.info(" resumable generated uri for file " + fileToSync.getFile().getName() + " is " + uploadURI);
        if (SyncStatus.getInstance() != null) {
            SyncStatus.getInstance().updateStatus(fileToSync.getFile().getAbsolutePath(), uploadURI, false);
        }
    }

    /**
     * retrieve size of the file already sent to servers
     *
     * @throws ClientProtocolException
     * @throws IOException
     * @throws GDocsServerError
     * @throws UnexpectedStatusCode
     */
    private void retrieveSizeAlreadySent() throws ClientProtocolException, IOException, GDocsServerError, UnexpectedStatusCode {
        logger.info("retrieve size received after recovering from an error");
        // create range to be sent see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16
        // --------------------------------------------------------------------------------------------
        StringBuilder range = new StringBuilder();
        range.append("bytes ");
        range.append("*");
        range.append("/");
        range.append(fileToSync.getFile().length());
        HttpPut httpPut = HttpHelper.createPutRequest(session, uploadURI.toString());
        httpPut.addHeader(HttpHelper.CONTENT_RANGE_HEADER, range.toString());
        HttpClient client = HttpHelper.getHttpClient();
        HttpResponse resp = client.execute(httpPut);
        int status = HttpHelper.getStatusCodeAndReleaseResource(resp);
        if (status != 308) {
            throw new UnexpectedStatusCode(status);
        }
        Header[] rangeReceived = resp.getHeaders("Range");
        if (rangeReceived == null || rangeReceived.length == 0) {
            totalSent = 0;
        } else {
            Matcher match = GDocsHTTPConstants.RANGE_PATTERN.matcher(rangeReceived[0].getValue());
            if (!match.matches()) {
                throw new GDocsServerError("unable to read range header received");
            }
            try {
                totalSent = Integer.parseInt(match.group(1)) + 1;
            } catch (NumberFormatException e) {
                throw new GDocsServerError("unable to parse range header received");
            }
        }
        fileToSync.uploadRate(totalSent);
        logger.info("size=" + totalSent);
    }

    /**
     * upload loop
     *
     * @throws IOException
     * @throws GDocsServerError
     * @throws UnexpectedStatusCode
     */
    public void doUpload() throws IOException, GDocsServerError, UnexpectedStatusCode {
        logger.info("starting upload for " + fileToSync.getFile().getName());
        long startSize = totalSent;
        long startTime = System.currentTimeMillis();
        do {
            if (Thread.currentThread().isInterrupted()) {
                break;
            }
            long previous = totalSent;
            FileInputStream fileToSyncStream = new FileInputStream(fileToSync.getFile());
            fileToSyncStream.skip(previous);
            try {
                int size = (int) Math.min(chunksize.intValue(), fileToSync.getFile().length() - previous);
                byte[] buffer = new byte[size];
                // create range to be sent see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16
                // --------------------------------------------------------------------------------------------
                StringBuilder range = new StringBuilder();
                range.append("bytes ");
                range.append(previous);
                range.append("-");
                range.append(previous + size - 1);
                range.append("/");
                range.append(fileToSync.getFile().length());
                HttpPut httpPut = HttpHelper.createPutRequest(session, uploadURI.toString());
                httpPut.addHeader(HttpHelper.CONTENT_TYPE, mimeType);


                // reading file part of file into buffer
                // -------------------------------------
                int fileRead = 0;
                do {
                    int cr = fileToSyncStream.read(buffer, fileRead, size - fileRead);
                    if (cr < 0) {
                        break;
                    }
                    fileRead += cr;
                } while (fileRead != size);

                if (fileRead != size) {
                    throw new IOException("Error reading file");
                }

                // adding buffer read to http content
                //  ---------------------------------
                EntityTemplate entity = new EntityTemplate(new BandwithSaverContentProducer(buffer));
                httpPut.setEntity(entity);
                httpPut.addHeader(HttpHelper.CONTENT_RANGE_HEADER, range.toString());

                HttpClient client = HttpHelper.getHttpClient();
                HttpResponse resp = client.execute(httpPut);
                int status = HttpHelper.getStatusCodeAndReleaseResource(resp);
                if (status != 308 && status != 201) {
                    throw new UnexpectedStatusCode(status);
                }
                // it works - we reset the retry count
                // -----------------------------------
                retry = 0;

                // we check if the resumable URI has changed
                // -----------------------------------------
                Header[] location = resp.getHeaders(HttpHelper.LOCATION_HEADER);
                if (location != null && location.length > 0) {
                    try {
                        uploadURI = new URI(location[0].getValue());
                    } catch (URISyntaxException e) {
                        throw new GDocsServerError("invalid URI received :" + location[0].getValue());
                    }
                    logger.info("new resumable generated uri for file " + fileToSync.getFile().getName() + " is " + uploadURI.toString());
                    if (SyncStatus.getInstance() != null) {
                        SyncStatus.getInstance().updateStatus(fileToSync.getFile().getAbsolutePath(), uploadURI, false);
                    }
                }

                // upload is not finished
                // we retrieve the total number of byte effectively send
                // -----------------------------------------------------
                if (status == 308) {
                    Header[] rangeReceived = resp.getHeaders("Range");
                    if (rangeReceived == null || rangeReceived.length == 0) {
                        throw new GDocsServerError("no range header received");
                    }
                    if (logger.isDebugEnabled()) {
                        logger.debug("range received :" + rangeReceived[0].getValue());
                    }
                    Matcher match = GDocsHTTPConstants.RANGE_PATTERN.matcher(rangeReceived[0].getValue());
                    if (!match.matches()) {
                        throw new GDocsServerError("unable to read range header received");
                    }
                    try {
                        totalSent = Long.parseLong(match.group(1)) + 1;
                        fileToSync.uploadRate(totalSent);
                    } catch (NumberFormatException e) {
                        throw new GDocsServerError("unable to parse range header received");
                    }
                } // end of sending the file
                // -----------------------
                else {
                    // file writed
                    if (SyncStatus.getInstance() != null) {
                        SyncStatus.getInstance().updateStatus(fileToSync.getFile().getAbsolutePath(), uploadURI, true);
                    }
                    logger.info("Send speed was " + (totalSent + buffer.length - startSize) / (System.currentTimeMillis() - startTime) * 1000 + " bytes per sec");
                    break;
                }

            } finally {
                fileToSyncStream.close();
            }
        } while (true);
    }

    @Override
    public Object call() throws Exception {
        boolean ok = false;
        try {
            fileToSync.startUpload();
            if (fileToSync.getFile().length() == 0) {
                fileToSync.endUpload();
                return null;
            }
            if (resume == false) {
                initiateUpload();
            } else {
                uploadURI = targetURI;
                retrieveSizeAlreadySent();
            }
            retry = 0;
            do {
                try {
                    if (retry != 0) {
                        retrieveSizeAlreadySent();
                    }
                    doUpload();
                    ok = true;
                    fileToSync.endUpload();
                    break;
                } catch (Exception e) {
                    if (e instanceof IOException) {
                        logger.error("receive io error : " + e.getMessage() + " sleep 1 sec");
                        Thread.sleep(1000);
                    } else if (e instanceof UnexpectedStatusCode) {
                        UnexpectedStatusCode e2 = (UnexpectedStatusCode) e;
                        if (e2.getErrorCode() >= 400 && e2.getErrorCode() < 500) {
                            logger.error("Error catched in thread loop - code=" + e2.getErrorCode() + " try with a new resumable uri", e2);
                            // retry with a new URI
                            // --------------------
                            totalSent = 0;
                            uploadURI = null;
                            initiateUpload();
                        } else if (e2.getErrorCode() >= 500) {
                            logger.error("receive http error : " + e2.getErrorCode() + " sleep 1 sec");
                            Thread.sleep(1000);
                        }
                    } else {
                        throw e;
                    }
                    retry++;
                }
            } while (retry < 5);
        } catch (Exception e) {
            logger.error("upload failed", e);
        } finally {
            if (ok == false) {
                fileToSync.uploadFailed();
            }
        }
        return (null);
    }
    private FileToSync fileToSync;
    private URI targetURI;
    private URI uploadURI;
    private Session session;
    private Integer chunksize;
    private long totalSent;
    private String mimeType;
    private boolean resume;
    private final static Logger logger = Logger.getLogger(HttpResumableUploader.class);
    private int retry = 0;
}
